/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.persistence.hibernate.spring.factory;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import javax.sql.DataSource;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.DialectFactory;
import org.hibernate.dialect.DialectFactory.DatabaseDialectMapper;
import org.hibernate.dialect.DialectFactory.VersionInsensitiveMapper;
import org.hibernate.event.DeleteEventListener;
import org.hibernate.event.SaveOrUpdateEventListener;
import org.hibernate.event.def.DefaultDeleteEventListener;
import org.hibernate.mapping.Table;
import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
import org.hibernate.tool.hbm2ddl.TableMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.LocalSessionFactoryBean;

import com.genia.toolbox.persistence.bean.Persistable;
import com.genia.toolbox.persistence.hibernate.dialect.DerbyDialect;
import com.genia.toolbox.persistence.hibernate.dialect.TeradataDialect;
import com.genia.toolbox.persistence.spring.configurator.PersistableConfigurator;

/**
 * A custom <code>LocalSessionFactoryBean</code> that allows to autoconfigure
 * hibernate with the spring configuration files.
 */
public class CustomLocalSessionFactoryBean
    extends LocalSessionFactoryBean
{

  /**
   * map of custom hibernate dialects, because {@link DialectFactory} MAPPERS
   * field is private.
   */
  private static final Map<String, DatabaseDialectMapper> DIALECT_MAPPERS = new HashMap<String, DatabaseDialectMapper>();

  /**
   * the <code>Log</code> variabe.
   */
  private static final Logger LOGGER = LoggerFactory.getLogger(CustomLocalSessionFactoryBean.class);

  // register custom dialects.
  static {
    DIALECT_MAPPERS.put("Apache Derby", new VersionInsensitiveMapper(DerbyDialect.class.getName()));
    DIALECT_MAPPERS.put("Teradata", new VersionInsensitiveMapper(TeradataDialect.class.getName()));
  }

  /**
   * the custom event listener to handle delete dependency.
   */
  private DeleteEventListener hibernateDeleteEventListener;

  /**
   * the custom event listener to handle save dependency.
   */
  private SaveOrUpdateEventListener hibernateSaveEventListener;

  /**
   * the custom event listener to handle save/update dependency.
   */
  private SaveOrUpdateEventListener hibernateSaveOrUpdateEventListener;



  /**
   * the custom event listener to handle update dependency.
   */
  private SaveOrUpdateEventListener hibernateUpdateEventListener;

  /**
   * The list of {@link PersistableConfigurator} that contains the mapping files
   * to register.
   */
  private List<PersistableConfigurator> persistableConfigurator;

  /**
   * getter for the hibernateDeleteEventListener property.
   * 
   * @return the hibernateDeleteEventListener
   */
  public DeleteEventListener getHibernateDeleteEventListener()
  {
    return hibernateDeleteEventListener;
  }



  /**
   * getter for the hibernateSaveEventListener property.
   * 
   * @return the hibernateSaveEventListener
   */
  public SaveOrUpdateEventListener getHibernateSaveEventListener()
  {
    return hibernateSaveEventListener;
  }



  /**
   * getter for the hibernateSaveEventListener property.
   * 
   * @return the hibernateSaveEventListener
   */
  public SaveOrUpdateEventListener getHibernateSaveOrUpdateEventListener()
  {
    return hibernateSaveOrUpdateEventListener;
  }



  /**
   * getter for the hibernateUpdateEventListener property.
   * 
   * @return the hibernateUpdateEventListener
   */
  public SaveOrUpdateEventListener getHibernateUpdateEventListener()
  {
    return hibernateUpdateEventListener;
  }



  /**
   * getter for the persistableConfigurator property.
   * 
   * @return the persistableConfigurator
   */
  public List<PersistableConfigurator> getPersistableConfigurator()
  {
    return persistableConfigurator;
  }



  /**
   * Set the DataSource to be used by the SessionFactory. If set, this will
   * override corresponding settings in Hibernate properties.
   * <p>
   * If this is set, the Hibernate settings should not define a connection
   * provider to avoid meaningless double configuration.
   * <p>
   * If using HibernateTransactionManager as transaction strategy, consider
   * proxying your target DataSource with a LazyConnectionDataSourceProxy. This
   * defers fetching of an actual JDBC Connection until the first JDBC Statement
   * gets executed, even within JDBC transactions (as performed by
   * HibernateTransactionManager). Such lazy fetching is particularly beneficial
   * for read-only operations, in particular if the chances of resolving the
   * result in the second-level cache are high.
   * <p>
   * As JTA and transactional JNDI DataSources already provide lazy enlistment
   * of JDBC Connections, LazyConnectionDataSourceProxy does not add value with
   * JTA (i.e. Spring's JtaTransactionManager) as transaction strategy.
   * 
   * @param dataSource
   *          the {@link DataSource} to use
   * @see #setUseTransactionAwareDataSource
   * @see org.springframework.orm.hibernate3.LocalDataSourceConnectionProvider
   * @see org.springframework.orm.hibernate3.HibernateTransactionManager
   * @see org.springframework.transaction.jta.JtaTransactionManager
   * @see org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy
   */
  @Override
  public void setDataSource(DataSource dataSource)
  {
    super.setDataSource(dataSource);
    if (getHibernateProperties().get("hibernate.dialect") == null) {
      try {
        Connection connection = dataSource.getConnection();
        String databaseName = connection.getMetaData().getDatabaseProductName();
        int databaseMajorVersion = connection.getMetaData().getDatabaseMajorVersion();
        connection.close();
        Dialect dialect = determineDialect(databaseName, databaseMajorVersion);
        if (dialect != null) {
          getHibernateProperties().put("hibernate.dialect", dialect.getClass().getCanonicalName());
        }
      }
      catch (SQLException e) {
        LOGGER.error(e.getMessage(), e);
      }
    }
  }



  /**
   * setter for the hibernateDeleteEventListener property.
   * 
   * @param hibernateDeleteEventListener
   *          the hibernateDeleteEventListener to set
   */
  public void setHibernateDeleteEventListener(DeleteEventListener hibernateDeleteEventListener)
  {
    this.hibernateDeleteEventListener = hibernateDeleteEventListener;
  }



  /**
   * Set Hibernate properties, such as "hibernate.dialect".
   * <p>
   * Can be used to override values in a Hibernate XML config file, or to
   * specify all necessary properties locally.
   * <p>
   * Note: Do not specify a transaction provider here when using Spring-driven
   * transactions. It is also advisable to omit connection provider settings and
   * use a Spring-set DataSource instead.
   * 
   * @param properties
   *          the {@link Properties} to set
   * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setHibernateProperties(java.util.Properties)
   */
  @Override
  public void setHibernateProperties(Properties properties)
  {
    for (Entry<Object, Object> propertyEntry : properties.entrySet()) {
      getHibernateProperties().put(propertyEntry.getKey(), propertyEntry.getValue());
    }
  }



  /**
   * setter for the hibernateSaveEventListener property.
   * 
   * @param hibernateSaveEventListener
   *          the hibernateSaveEventListener to set
   */
  public void setHibernateSaveEventListener(SaveOrUpdateEventListener hibernateSaveEventListener)
  {
    this.hibernateSaveEventListener = hibernateSaveEventListener;
  }



  /**
   * setter for the hibernateSaveEventListener property.
   * 
   * @param hibernateSaveEventListener
   *          the hibernateSaveEventListener to set
   */
  public void setHibernateSaveOrUpdateEventListener(SaveOrUpdateEventListener hibernateSaveEventListener)
  {
    this.hibernateSaveOrUpdateEventListener = hibernateSaveEventListener;
  }



  /**
   * setter for the hibernateUpdateEventListener property.
   * 
   * @param hibernateUpdateEventListener
   *          the hibernateUpdateEventListener to set
   */
  public void setHibernateUpdateEventListener(SaveOrUpdateEventListener hibernateUpdateEventListener)
  {
    this.hibernateUpdateEventListener = hibernateUpdateEventListener;
  }



  /**
   * setter for the persistableConfigurator property.
   * 
   * @param persistableConfigurator
   *          the persistableConfigurator to set
   */
  public void setPersistableConfigurator(List<PersistableConfigurator> persistableConfigurator)
  {
    this.persistableConfigurator = persistableConfigurator;
  }



  /**
   * Rewriting the method to call the creation script is no table already
   * exists. That will allow index to be generated.
   * 
   * @throws DataAccessException
   *           in case of script execution errors
   * @see #setSchemaUpdate
   * @see org.hibernate.cfg.Configuration#generateSchemaUpdateScript
   * @see org.hibernate.tool.hbm2ddl.SchemaUpdate
   * @see LocalSessionFactoryBean#updateDatabaseSchema()
   */
  @Override
  public void updateDatabaseSchema()
      throws DataAccessException
  {
    logger.info("Updating database schema for Hibernate SessionFactory");
    HibernateTemplate hibernateTemplate = new HibernateTemplate(getSessionFactory());
    hibernateTemplate.setFlushMode(HibernateTemplate.FLUSH_NEVER);
    hibernateTemplate.execute(new HibernateCallback()
    {
      public Object doInHibernate(Session session)
          throws HibernateException, SQLException
      {
        @SuppressWarnings("deprecation")
        Connection con = session.connection();
        Dialect dialect = Dialect.getDialect(getConfiguration().getProperties());
        DatabaseMetadata metadata = new DatabaseMetadata(con, dialect);
        // We need to call generateSchemaUpdateScript anyway because
        // secondPassCompile is a protected method
        String[] sql = getConfiguration().generateSchemaUpdateScript(dialect, metadata);
        if (doCreateSchema(metadata)) {
          sql = getConfiguration().generateSchemaCreationScript(dialect);
        }
        executeSchemaScript(con, sql);
        return null;
      }
    });
  }



  /**
   * Determine the appropriate Dialect to use given the database product name
   * and major version.
   * 
   * @param databaseName
   *          The name of the database product (obtained from metadata).
   * @param databaseMajorVersion
   *          The major version of the database product (obtained from
   *          metadata).
   * @return An appropriate dialect instance.
   */
  private Dialect determineDialect(String databaseName, int databaseMajorVersion)
  {
    if (DIALECT_MAPPERS.containsKey(databaseName)) {
      DatabaseDialectMapper mapper = DIALECT_MAPPERS.get(databaseName);

      if (mapper != null) {
        String dialectName = mapper.getDialectClass(databaseMajorVersion);
        return DialectFactory.buildDialect(dialectName);
      }
    }
    return DialectFactory.determineDialect(databaseName, databaseMajorVersion);
  }



  /**
   * returns <code>true</code> if no table needed by hibernate are already
   * created and the schema must be created from scratch.
   * 
   * @param databaseMetadata
   *          the metadata of the database
   * @return <code>true</code> if no table needed by hibernate are already
   *         created and the schema must be created from scratch
   */
  private boolean doCreateSchema(DatabaseMetadata databaseMetadata)
  {
    String defaultCatalog = getConfiguration().getProperty(Environment.DEFAULT_CATALOG);
    String defaultSchema = getConfiguration().getProperty(Environment.DEFAULT_SCHEMA);

    Iterator<?> iter = getConfiguration().getTableMappings();
    while (iter.hasNext()) {
      Table table = (Table) iter.next();
      if (table.isPhysicalTable()) {

        TableMetadata tableInfo = databaseMetadata.getTableMetadata(table.getName(), (table.getSchema() == null) ? defaultSchema : table.getSchema(), (table.getCatalog() == null) ? defaultCatalog
            : table.getCatalog(), table.isQuoted()

        );
        if (tableInfo != null) {
          return false;
        }
      }
    }
    return true;
  }



  /**
   * this method allow us to configure automatically hibernate, depending on the
   * configurator that are declared in the spring configuration files.
   * 
   * @param configuration
   *          the <code>Configuration</code> object used by the hibernate API
   * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#postProcessConfiguration(org.hibernate.cfg.Configuration)
   */
  @Override
  protected void postProcessConfiguration(final Configuration configuration)
  {
    super.postProcessConfiguration(configuration);
    if (LocalSessionFactoryBean.getConfigTimeTransactionManager() != null) {
      // We need to add a TRANSACTION_STRATEGY
      configuration.setProperty(Environment.TRANSACTION_STRATEGY, SpringJTATransactionFactory.class.getName());
    }
    for (final PersistableConfigurator configurator : getPersistableConfigurator()) {
      for (final Class<? extends Persistable<?>> type : configurator.getPersistentTypes()) {
        configuration.addClass(type);
      }
    }
    configuration.getEventListeners().setSaveEventListeners(new SaveOrUpdateEventListener[] { getHibernateSaveOrUpdateEventListener() });
    configuration.getEventListeners().setSaveOrUpdateEventListeners(new SaveOrUpdateEventListener[] { getHibernateSaveOrUpdateEventListener() });
    configuration.getEventListeners().setUpdateEventListeners(new SaveOrUpdateEventListener[] { getHibernateSaveOrUpdateEventListener() });
    configuration.getEventListeners().setDeleteEventListeners(new DeleteEventListener[] { getHibernateDeleteEventListener(), new DefaultDeleteEventListener() });
  }

}
